package io.keen.client.java;

import com.sun.mail.imap.IMAPStore;
import io.keen.client.java.exceptions.InvalidEventCollectionException;
import io.keen.client.java.exceptions.InvalidEventException;
import io.keen.client.java.exceptions.NoWriteKeyException;
import io.keen.client.java.exceptions.ServerException;
import io.keen.client.java.http.HttpHandler;
import io.keen.client.java.http.OutputSource;
import io.keen.client.java.http.Request;
import io.keen.client.java.http.Response;
import io.keen.client.java.http.UrlConnectionHttpHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.Proxy;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class KeenClient {
    private static final DateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
    private String baseUrl;
    private KeenProject defaultProject;
    private final KeenEventStore eventStore;
    private Map<String, Object> globalProperties;
    private GlobalPropertiesEvaluator globalPropertiesEvaluator;
    private final HttpHandler httpHandler;
    private boolean isActive;
    private boolean isDebugMode;
    private final KeenJsonHandler jsonHandler;
    private int maxRecursiveCalls;
    private int maxUploadSizeBytes;
    private Proxy proxy;
    private final Executor publishExecutor;

    public static abstract class Builder {
        private KeenEventStore eventStore;
        private HttpHandler httpHandler;
        private KeenJsonHandler jsonHandler;
        private int maxRecursiveCalls = 8;
        private int maxUploadSizeBytes = 750000;
        private Executor publishExecutor;

        protected abstract KeenJsonHandler getDefaultJsonHandler() throws Exception;

        protected HttpHandler getDefaultHttpHandler() throws Exception {
            return new UrlConnectionHttpHandler();
        }

        public void setHttpHandler(HttpHandler httpHandler) {
            this.httpHandler = httpHandler;
        }

        public Builder withHttpHandler(HttpHandler httpHandler) {
            setHttpHandler(httpHandler);
            return this;
        }

        protected KeenEventStore getDefaultEventStore() throws Exception {
            return new RamEventStore();
        }

        protected Executor getDefaultPublishExecutor() throws Exception {
            return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        }

        public void setMaxRecursiveCalls(int maxRecursiveCalls) {
            this.maxRecursiveCalls = maxRecursiveCalls;
        }

        public void setMaxUploadSizeBytes(int maxUploadSizeBytes) {
            this.maxUploadSizeBytes = maxUploadSizeBytes;
        }

        public KeenClient build() {
            try {
                if (this.httpHandler == null) {
                    this.httpHandler = getDefaultHttpHandler();
                }
            } catch (Exception e) {
                KeenLogging.log("Exception building HTTP handler: " + e.getMessage());
            }
            try {
                if (this.jsonHandler == null) {
                    this.jsonHandler = getDefaultJsonHandler();
                }
            } catch (Exception e2) {
                KeenLogging.log("Exception building JSON handler: " + e2.getMessage());
            }
            try {
                if (this.eventStore == null) {
                    this.eventStore = getDefaultEventStore();
                }
            } catch (Exception e22) {
                KeenLogging.log("Exception building event store: " + e22.getMessage());
            }
            try {
                if (this.publishExecutor == null) {
                    this.publishExecutor = getDefaultPublishExecutor();
                }
            } catch (Exception e222) {
                KeenLogging.log("Exception building publish executor: " + e222.getMessage());
            }
            return buildInstance();
        }

        protected KeenClient buildInstance() {
            return new KeenClient(this);
        }
    }

    private enum ClientSingleton {
        INSTANCE;
        
        KeenClient client;
    }

    public static KeenClient client() {
        if (ClientSingleton.INSTANCE.client != null) {
            return ClientSingleton.INSTANCE.client;
        }
        throw new IllegalStateException("Please call KeenClient.initialize() before requesting the client.");
    }

    public static void initialize(KeenClient client) {
        if (client == null) {
            throw new IllegalArgumentException("Client must not be null");
        } else if (ClientSingleton.INSTANCE.client == null) {
            ClientSingleton.INSTANCE.client = client;
        }
    }

    public void queueEvent(String eventCollection, Map<String, Object> event) {
        queueEvent(eventCollection, event, null);
    }

    public void queueEvent(String eventCollection, Map<String, Object> event, Map<String, Object> keenProperties) {
        queueEvent(null, eventCollection, event, keenProperties, null);
    }

    public void queueEvent(KeenProject project, String eventCollection, Map<String, Object> event, Map<String, Object> keenProperties, KeenCallback callback) {
        if (!this.isActive) {
            handleLibraryInactive(callback);
        } else if (project == null && this.defaultProject == null) {
            handleFailure(null, new IllegalStateException("No project specified, but no default project found"));
        } else {
            KeenProject useProject;
            if (project == null) {
                useProject = this.defaultProject;
            } else {
                useProject = project;
            }
            try {
                Map<String, Object> newEvent = validateAndBuildEvent(useProject, eventCollection, event, keenProperties);
                StringWriter writer = new StringWriter();
                this.jsonHandler.writeJson(writer, newEvent);
                String jsonEvent = writer.toString();
                KeenUtils.closeQuietly(writer);
                this.eventStore.store(useProject.getProjectId(), eventCollection, jsonEvent);
                handleSuccess(callback);
            } catch (Exception e) {
                handleFailure(callback, e);
            }
        }
    }

    public synchronized void sendQueuedEvents(KeenProject project, KeenCallback callback) {
        sendQueuedEvents(project, callback, this.maxRecursiveCalls);
    }

    public synchronized void sendQueuedEvents(KeenProject project, KeenCallback callback, int maxCallsRemaining) {
        if (maxCallsRemaining <= 0) {
            KeenLogging.log("maxCalls limit; returning");
        } else {
            KeenLogging.log("sendQueuedEvents maxCallsRemaining = " + maxCallsRemaining);
            if (this.isActive) {
                KeenProject useProject;
                if (project == null) {
                    if (this.defaultProject == null) {
                        handleFailure(null, new IllegalStateException("No project specified, but no default project found"));
                    }
                }
                if (project == null) {
                    useProject = this.defaultProject;
                } else {
                    useProject = project;
                }
                try {
                    Map<String, List<Object>> eventHandles = this.eventStore.getHandles(useProject.getProjectId());
                    Map<String, List<Map<String, Object>>> events = new HashMap();
                    boolean eventsRemaining = buildEventMap(eventHandles, events);
                    String response = publishAll(useProject, events);
                    boolean handledResponse = false;
                    if (response != null) {
                        try {
                            handleAddEventsResponse(eventHandles, response);
                            handledResponse = true;
                        } catch (Exception e) {
                            KeenLogging.log("Error handling response to batch publish: " + e.getMessage());
                        }
                    }
                    if (eventsRemaining && handledResponse) {
                        sendQueuedEvents(project, callback, maxCallsRemaining - 1);
                    } else {
                        handleSuccess(callback);
                    }
                } catch (Exception e2) {
                    handleFailure(callback, e2);
                }
            } else {
                handleLibraryInactive(callback);
            }
        }
    }

    public KeenProject getDefaultProject() {
        return this.defaultProject;
    }

    public void setDefaultProject(KeenProject defaultProject) {
        this.defaultProject = defaultProject;
    }

    public String getBaseUrl() {
        return this.baseUrl;
    }

    public GlobalPropertiesEvaluator getGlobalPropertiesEvaluator() {
        return this.globalPropertiesEvaluator;
    }

    public Map<String, Object> getGlobalProperties() {
        return this.globalProperties;
    }

    public void setGlobalProperties(Map<String, Object> globalProperties) {
        this.globalProperties = globalProperties;
    }

    protected KeenClient(Builder builder) {
        this(builder, new Environment());
    }

    KeenClient(Builder builder, Environment env) {
        this.isActive = true;
        this.httpHandler = builder.httpHandler;
        this.jsonHandler = builder.jsonHandler;
        this.eventStore = builder.eventStore;
        this.publishExecutor = builder.publishExecutor;
        if (this.httpHandler == null || this.jsonHandler == null || this.eventStore == null || this.publishExecutor == null) {
            setActive(false);
        }
        this.baseUrl = "https://api.keen.io";
        this.globalPropertiesEvaluator = null;
        this.globalProperties = null;
        this.maxRecursiveCalls = builder.maxRecursiveCalls;
        this.maxUploadSizeBytes = builder.maxUploadSizeBytes;
        if (env.getKeenProjectId() != null) {
            this.defaultProject = new KeenProject(env);
        }
    }

    protected void setActive(boolean isActive) {
        this.isActive = isActive;
        KeenLogging.log("Keen Client set to " + (isActive ? "active" : "inactive"));
    }

    protected Map<String, Object> validateAndBuildEvent(KeenProject project, String eventCollection, Map<String, Object> event, Map<String, Object> keenProperties) {
        if (project.getWriteKey() == null) {
            throw new NoWriteKeyException("You can't send events to Keen IO if you haven't set a write key.");
        }
        validateEventCollection(eventCollection);
        validateEvent(event);
        KeenLogging.log(String.format(Locale.US, "Adding event to collection: %s", new Object[]{eventCollection}));
        Map<String, Object> newEvent = new HashMap();
        String timestamp = ISO_8601_FORMAT.format(Calendar.getInstance().getTime());
        if (keenProperties == null) {
            keenProperties = new HashMap();
            keenProperties.put("timestamp", timestamp);
        } else if (!keenProperties.containsKey("timestamp")) {
            Map<String, Object> keenProperties2 = new HashMap(keenProperties);
            keenProperties2.put("timestamp", timestamp);
            keenProperties = keenProperties2;
        }
        newEvent.put("keen", keenProperties);
        Map<String, Object> globalProperties = getGlobalProperties();
        if (globalProperties != null) {
            newEvent.putAll(globalProperties);
        }
        GlobalPropertiesEvaluator globalPropertiesEvaluator = getGlobalPropertiesEvaluator();
        if (globalPropertiesEvaluator != null) {
            Map<String, Object> props = globalPropertiesEvaluator.getGlobalProperties(eventCollection);
            if (props != null) {
                newEvent.putAll(props);
            }
        }
        newEvent.putAll(event);
        return newEvent;
    }

    private void validateEventCollection(String eventCollection) {
        if (eventCollection == null || eventCollection.length() == 0) {
            throw new InvalidEventCollectionException("You must specify a non-null, non-empty event collection: " + eventCollection);
        } else if (eventCollection.length() > 256) {
            throw new InvalidEventCollectionException("An event collection name cannot be longer than 256 characters.");
        }
    }

    private void validateEvent(Map<String, Object> event) {
        validateEvent(event, 0);
    }

    private void validateEvent(Map<String, Object> event, int depth) {
        if (depth == 0) {
            if (event == null || event.size() == 0) {
                throw new InvalidEventException("You must specify a non-null, non-empty event.");
            } else if (event.containsKey("keen")) {
                throw new InvalidEventException("An event cannot contain a root-level property named 'keen'.");
            }
        } else if (depth > IMAPStore.RESPONSE) {
            throw new InvalidEventException("An event's depth (i.e. layers of nesting) cannot exceed 1000");
        }
        for (Entry<String, Object> entry : event.entrySet()) {
            String key = (String) entry.getKey();
            if (key.contains(".")) {
                throw new InvalidEventException("An event cannot contain a property with the period (.) character in it.");
            } else if (key.length() > 256) {
                throw new InvalidEventException("An event cannot contain a property name longer than 256 characters.");
            } else {
                validateEventValue(entry.getValue(), depth);
            }
        }
    }

    private void validateEventValue(Object value, int depth) {
        if (value instanceof String) {
            if (((String) value).length() >= 10000) {
                throw new InvalidEventException("An event cannot contain a string property value longer than 10,000 characters.");
            }
        } else if (value instanceof Map) {
            validateEvent((Map) value, depth + 1);
        } else if (value instanceof Iterable) {
            for (Object listElement : (Iterable) value) {
                validateEventValue(listElement, depth);
            }
        }
    }

    private boolean buildEventMap(Map<String, List<Object>> eventHandles, Map<String, List<Map<String, Object>>> result) throws IOException {
        boolean exceededMaxUploadSize = false;
        int uploadSize = 0;
        for (Entry<String, List<Object>> entry : eventHandles.entrySet()) {
            String eventCollection = (String) entry.getKey();
            List<Object> handles = (List) entry.getValue();
            if (!(handles == null || handles.size() == 0 || exceededMaxUploadSize)) {
                List<Map<String, Object>> events = new ArrayList(handles.size());
                for (Object handle : handles) {
                    String jsonEvent = this.eventStore.get(handle);
                    uploadSize += jsonEvent.length();
                    if (uploadSize > this.maxUploadSizeBytes) {
                        exceededMaxUploadSize = true;
                        break;
                    }
                    StringReader reader = new StringReader(jsonEvent);
                    Map<String, Object> event = this.jsonHandler.readJson(reader);
                    KeenUtils.closeQuietly(reader);
                    events.add(event);
                }
                result.put(eventCollection, events);
            }
        }
        return exceededMaxUploadSize;
    }

    private String publishAll(KeenProject project, Map<String, List<Map<String, Object>>> events) throws IOException {
        return publishObject(project, new URL(String.format(Locale.US, "%s/%s/projects/%s/events", new Object[]{getBaseUrl(), "3.0", project.getProjectId()})), events);
    }

    private synchronized String publishObject(KeenProject project, URL url, final Map<String, ?> requestData) throws IOException {
        String str;
        if (requestData != null) {
            if (requestData.size() != 0) {
                OutputSource source = new OutputSource() {
                    public void writeTo(OutputStream out) throws IOException {
                        KeenClient.this.jsonHandler.writeJson(new OutputStreamWriter(out, "UTF-8"), requestData);
                    }
                };
                if (KeenLogging.isLoggingEnabled()) {
                    try {
                        StringWriter writer = new StringWriter();
                        this.jsonHandler.writeJson(writer, requestData);
                        String request = writer.toString();
                        KeenLogging.log(String.format(Locale.US, "Sent request '%s' to URL '%s'", new Object[]{request, url.toString()}));
                    } catch (IOException e) {
                        KeenLogging.log("Couldn't log event written to file: ");
                        e.printStackTrace();
                    }
                }
                URL url2 = url;
                Response response = this.httpHandler.execute(new Request(url2, "POST", project.getWriteKey(), source, this.proxy));
                if (KeenLogging.isLoggingEnabled()) {
                    KeenLogging.log(String.format(Locale.US, "Received response: '%s' (%d)", new Object[]{response.body, Integer.valueOf(response.statusCode)}));
                }
                if (response.isSuccess()) {
                    str = response.body;
                } else {
                    throw new ServerException(response.body);
                }
            }
        }
        KeenLogging.log("No API calls were made because there were no events to upload");
        str = null;
        return str;
    }

    private void handleAddEventsResponse(Map<String, List<Object>> handles, String response) throws IOException {
        for (Entry<String, Object> entry : this.jsonHandler.readJson(new StringReader(response)).entrySet()) {
            List<Object> collectionHandles = (List) handles.get((String) entry.getKey());
            int index = 0;
            for (Map<String, Object> eventResult : (List) entry.getValue()) {
                boolean removeCacheEntry = true;
                if (!((Boolean) eventResult.get("success")).booleanValue()) {
                    Map errorDict = (Map) eventResult.get("error");
                    String errorCode = (String) errorDict.get("name");
                    if (errorCode.equals("InvalidCollectionNameError") || errorCode.equals("InvalidPropertyNameError") || errorCode.equals("InvalidPropertyValueError")) {
                        removeCacheEntry = true;
                        KeenLogging.log("An invalid event was found. Deleting it. Error: " + errorDict.get("description"));
                    } else {
                        String description = (String) errorDict.get("description");
                        removeCacheEntry = false;
                        KeenLogging.log(String.format(Locale.US, "The event could not be inserted for some reason. Error name and description: %s %s", new Object[]{errorCode, description}));
                    }
                }
                if (removeCacheEntry) {
                    Object handle = collectionHandles.get(index);
                    try {
                        this.eventStore.remove(handle);
                    } catch (IOException e) {
                        KeenLogging.log("Failed to remove object '" + handle + "' from cache");
                    }
                }
                index++;
            }
        }
    }

    private void handleSuccess(KeenCallback callback) {
        if (callback != null) {
            try {
                callback.onSuccess();
            } catch (Exception e) {
            }
        }
    }

    private void handleFailure(KeenCallback callback, Exception e) {
        if (!this.isDebugMode) {
            KeenLogging.log("Encountered error: " + e.getMessage());
            if (callback != null) {
                try {
                    callback.onFailure(e);
                } catch (Exception e2) {
                }
            }
        } else if (e instanceof RuntimeException) {
            throw ((RuntimeException) e);
        } else {
            throw new RuntimeException(e);
        }
    }

    private void handleLibraryInactive(KeenCallback callback) {
        handleFailure(callback, new IllegalStateException("The Keen library failed to initialize properly and is inactive"));
    }
}
